#!/usr/bin/env node

const { Command } = require("commander");
const fs = require("fs-extra");
const path = require("node:path");
const { glob } = require("glob");
const { minimatch } = require("minimatch");

/**
 * 递归发现目录中的所有文件
 * @param {string} rootDir - 要扫描的根目录
 * @returns {Promise<string[]>} 文件路径数组
 */
async function discoverFiles(rootDir) {
    try {
        const gitignorePath = path.join(rootDir, ".gitignore");
        const gitignorePatterns = await parseGitignore(gitignorePath);

        // Common gitignore patterns that should always be ignored
        const commonIgnorePatterns = [
            // Version control
            ".git/**",
            ".svn/**",
            ".hg/**",
            ".bzr/**",

            // Dependencies
            "node_modules/**",
            "bower_components/**",
            "vendor/**",
            "packages/**",

            // Build outputs
            "build/**",
            "dist/**",
            "out/**",
            "target/**",
            "bin/**",
            "obj/**",
            "release/**",
            "debug/**",

            // Environment and config
            ".env",
            ".env.*",
            "*.env",
            ".config",

            // Logs
            "logs/**",
            "*.log",
            "npm-debug.log*",
            "yarn-debug.log*",
            "yarn-error.log*",
            "lerna-debug.log*",

            // Coverage and testing
            "coverage/**",
            ".nyc_output/**",
            ".coverage/**",
            "test-results/**",
            "junit.xml",

            // Cache directories
            ".cache/**",
            ".tmp/**",
            ".temp/**",
            "tmp/**",
            "temp/**",
            ".sass-cache/**",
            ".eslintcache",
            ".stylelintcache",

            // OS generated files
            ".DS_Store",
            ".DS_Store?",
            "._*",
            ".Spotlight-V100",
            ".Trashes",
            "ehthumbs.db",
            "Thumbs.db",
            "desktop.ini",

            // IDE and editor files
            ".vscode/**",
            ".idea/**",
            "*.swp",
            "*.swo",
            "*~",
            ".project",
            ".classpath",
            ".settings/**",
            "*.sublime-project",
            "*.sublime-workspace",

            // Package manager files
            "package-lock.json",
            "yarn.lock",
            "pnpm-lock.yaml",
            "composer.lock",
            "Pipfile.lock",

            // Runtime and compiled files
            "*.pyc",
            "*.pyo",
            "*.pyd",
            "__pycache__/**",
            "*.class",
            "*.jar",
            "*.war",
            "*.ear",
            "*.o",
            "*.so",
            "*.dll",
            "*.exe",

            // Documentation build
            "_site/**",
            ".jekyll-cache/**",
            ".jekyll-metadata",

            // Flattener specific outputs
            "flattened-codebase.xml",
            "repomix-output.xml",
        ];

        const combinedIgnores = [...gitignorePatterns, ...commonIgnorePatterns];

        // 使用 glob 递归发现所有文件，排除常见的忽略模式
        const files = await glob("**/*", {
            cwd: rootDir,
            nodir: true, // 仅文件，不包括目录
            dot: true, // 包含隐藏文件
            follow: false, // 不遵循符号链接
            ignore: combinedIgnores,
        });

        return files.map((file) => path.resolve(rootDir, file));
    } catch (error) {
        console.error("Error discovering files:", error.message);
        return [];
    }
}

/**
 * 解析 .gitignore 文件并返回忽略模式
 * @param {string} gitignorePath - .gitignore 文件的路径
 * @returns {Promise<string[]>} 忽略模式数组
 */
async function parseGitignore(gitignorePath) {
    try {
        if (!(await fs.pathExists(gitignorePath))) {
            return [];
        }

        const content = await fs.readFile(gitignorePath, "utf8");
        return content
            .split("\n")
            .map((line) => line.trim())
            .filter((line) => line && !line.startsWith("#")) // 移除空行和注释
            .map((pattern) => {
                // 将 gitignore 模式转换为 glob 模式
                if (pattern.endsWith("/")) {
                    return pattern + "**";
                }
                return pattern;
            });
    } catch (error) {
        console.error("Error parsing .gitignore:", error.message);
        return [];
    }
}

/**
 * Check if a file is binary using file command and heuristics
 * @param {string} filePath - Path to the file
 * @returns {Promise<boolean>} True if file is binary
 */
async function isBinaryFile(filePath) {
    try {
        // First check by file extension
        const binaryExtensions = [
            ".jpg",
            ".jpeg",
            ".png",
            ".gif",
            ".bmp",
            ".ico",
            ".svg",
            ".pdf",
            ".doc",
            ".docx",
            ".xls",
            ".xlsx",
            ".ppt",
            ".pptx",
            ".zip",
            ".tar",
            ".gz",
            ".rar",
            ".7z",
            ".exe",
            ".dll",
            ".so",
            ".dylib",
            ".mp3",
            ".mp4",
            ".avi",
            ".mov",
            ".wav",
            ".ttf",
            ".otf",
            ".woff",
            ".woff2",
            ".bin",
            ".dat",
            ".db",
            ".sqlite",
        ];

        const ext = path.extname(filePath).toLowerCase();
        if (binaryExtensions.includes(ext)) {
            return true;
        }

        // 对于没有明确扩展名的文件，尝试读取一小部分内容
        const stats = await fs.stat(filePath);
        if (stats.size === 0) {
            return false; // 空文件被认为是文本文件
        }

        // 读取前 1024 字节以检查是否包含 null 字节
        const sampleSize = Math.min(1024, stats.size);
        const buffer = await fs.readFile(filePath, {
            encoding: null,
            flag: "r",
        });
        const sample = buffer.slice(0, sampleSize);
        // If we find null bytes, it's likely binary
        return sample.includes(0);
    } catch (error) {
        console.warn(
            `在 ${filePath} 中检查是否为二进制文件时出错：${error.message}`
        );
        return false; // 无法确定时默认认为是文本文件
    }
}

/**
 * 从文本文件中读取内容并聚合
 * @param {string[]} files - 文件路径数组
 * @param {string} rootDir - 根目录
 * @param {Object} spinner - 可选的进度指示器实例
 * @returns {Promise<Object>} 包含文件内容和元数据的对象
 */
async function aggregateFileContents(files, rootDir, spinner = null) {
    const results = {
        textFiles: [],
        binaryFiles: [],
        errors: [],
        totalFiles: files.length,
        processedFiles: 0,
    };

    for (const filePath of files) {
        try {
            const relativePath = path.relative(rootDir, filePath);

            // Update progress indicator
            if (spinner) {
                spinner.text = `Processing file ${results.processedFiles + 1}/${results.totalFiles}: ${relativePath}`;
            }

            const isBinary = await isBinaryFile(filePath);

            if (isBinary) {
                results.binaryFiles.push({
                    path: relativePath,
                    absolutePath: filePath,
                    size: (await fs.stat(filePath)).size,
                });
            } else {
                // Read text file content
                const content = await fs.readFile(filePath, "utf8");
                results.textFiles.push({
                    path: relativePath,
                    absolutePath: filePath,
                    content: content,
                    size: content.length,
                    lines: content.split("\n").length,
                });
            }

            results.processedFiles++;
        } catch (error) {
            const relativePath = path.relative(rootDir, filePath);
            const errorInfo = {
                path: relativePath,
                absolutePath: filePath,
                error: error.message,
            };

            results.errors.push(errorInfo);

            // Log warning without interfering with spinner
            if (spinner) {
                spinner.warn(
                    `警告：无法读取文件 ${relativePath}：${error.message}`
                );
            } else {
                console.warn(
                    `警告：无法读取文件 ${relativePath}：${error.message}`
                );
            }

            results.processedFiles++;
        }
    }

    return results;
}

/**
 * 生成包含聚合文件内容的 XML 输出，使用流式写入以最小化内存占用
 * @param {Object} aggregatedContent - 聚合后的内容对象
 * @param {string} outputPath - 输出文件路径
 * @returns {Promise<void>} 完成写入时解析的 Promise
 */
async function generateXMLOutput(aggregatedContent, outputPath) {
    const { textFiles } = aggregatedContent;

    // 创建写入流，用于高效内存使用
    const writeStream = fs.createWriteStream(outputPath, { encoding: "utf8" });

    return new Promise((resolve, reject) => {
        writeStream.on("error", reject);
        writeStream.on("finish", resolve);

        // 写入 XML 头
        writeStream.write('<?xml version="1.0" encoding="UTF-8"?>\n');
        writeStream.write("<files>\n");

        // 按顺序处理文件，以最小化内存占用
        let fileIndex = 0;

        const writeNextFile = () => {
            if (fileIndex >= textFiles.length) {
                // 所有文件处理完成，关闭 XML 标签和流
                writeStream.write("</files>\n");
                writeStream.end();
                return;
            }

            const file = textFiles[fileIndex];
            fileIndex++;

            // 写入文件 opening tag
            writeStream.write(`  <file path="${escapeXml(file.path)}">`);

            // 使用 CDATA 包装代码内容，处理 CDATA 结束序列
            if (file.content?.trim()) {
                const indentedContent = indentFileContent(file.content);
                if (file.content.includes("]]>")) {
                    // 如果内容包含 ]]>, 则将其拆分为多个部分并分别包装在 CDATA 中
                    writeStream.write(splitAndWrapCDATA(indentedContent));
                } else {
                    writeStream.write(`<![CDATA[\n${indentedContent}\n    ]]>`);
                }
            } else if (file.content) {
                // 处理空内容或仅包含空格的情况
                const indentedContent = indentFileContent(file.content);
                writeStream.write(`<![CDATA[\n${indentedContent}\n    ]]>`);
            }

            // 写入文件 closing tag
            writeStream.write("</file>\n");

            // 继续处理下一个文件，避免栈溢出
            setImmediate(writeNextFile);
        };

        // 开始处理文件
        writeNextFile();
    });
}

/**
 * 转义 XML 特殊字符，用于属性值
 * @param {string} str - 要转义的字符串
 * @returns {string} 转义后的字符串
 */
function escapeXml(str) {
    if (typeof str !== "string") {
        return String(str);
    }
    return str
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&apos;");
}

/**
 * 缩进文件内容，每个行前添加 4 个空格
 * @param {string} content - 要缩进的内容
 * @returns {string} 缩进后的内容
 */
function indentFileContent(content) {
    if (typeof content !== "string") {
        return String(content);
    }

    // 按行分割内容并在每一行前添加 4 个空格
    return content
        .split("\n")
        .map((line) => `    ${line}`)
        .join("\n");
}

/**
 * 处理包含 ]]> 的内容，将其拆分为多个部分并分别包装在 CDATA 中
 * @param {string} content - 要处理的内容
 * @returns {string} 处理后的内容，每个部分都包装在 CDATA 中
 */
function splitAndWrapCDATA(content) {
    if (typeof content !== "string") {
        return String(content);
    }

    // 按 ]]> 拆分内容，避免在 CDATA 中直接包含 ]]>
    const parts = content.split("]]>");
    let cdataContent = "";

    parts.forEach((part, index) => {
        cdataContent += part;
        if (index < parts.length - 1) {
            // 在每个 ]]> 后添加 ]]]]><![CDATA[> 以转义 ]]> 字符
            cdataContent += "]]]]><![CDATA[>";
        }
    });

    return `<![CDATA[
${cdataContent}
    ]]>`;
}

/**
 * 计算处理后的文件统计信息
 * @param {Object} aggregatedContent - 聚合后的内容对象
 * @param {number} xmlFileSize - 生成的 XML 文件大小（字节）
 * @returns {Object} 统计信息对象
 */
function calculateStatistics(aggregatedContent, xmlFileSize) {
    const { textFiles, binaryFiles, errors } = aggregatedContent;

    // 计算文本文件总大小（字节）
    const totalTextSize = textFiles.reduce((sum, file) => sum + file.size, 0);
    // 计算二进制文件总大小（字节）
    const totalBinarySize = binaryFiles.reduce(
        (sum, file) => sum + file.size,
        0
    );
    // 计算总大小（字节）
    const totalSize = totalTextSize + totalBinarySize;

    // 计算总代码行数
    const totalLines = textFiles.reduce((sum, file) => sum + file.lines, 0);

    // 估计令牌数量（粗略近似：1 个令牌 ≈ 4 个字符）
    const estimatedTokens = Math.ceil(xmlFileSize / 4);

    // 格式化文件大小
    const formatSize = (bytes) => {
        if (bytes < 1024) return `${bytes} B`;
        if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
        return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
    };

    return {
        totalFiles: textFiles.length + binaryFiles.length,
        textFiles: textFiles.length,
        binaryFiles: binaryFiles.length,
        errorFiles: errors.length,
        totalSize: formatSize(totalSize),
        xmlSize: formatSize(xmlFileSize),
        totalLines,
        estimatedTokens: estimatedTokens.toLocaleString(),
    };
}

/**
 * 基于 .gitignore 模式过滤文件
 * @param {string[]} files - 文件路径数组
 * @param {string} rootDir - 根目录
 * @returns {Promise<string[]>} 过滤后的文件路径数组
 */
async function filterFiles(files, rootDir) {
    const gitignorePath = path.join(rootDir, ".gitignore");
    const ignorePatterns = await parseGitignore(gitignorePath);

    if (ignorePatterns.length === 0) {
        return files;
    }

    // 转换为相对路径进行模式匹配
    const relativeFiles = files.map((file) => path.relative(rootDir, file));

    // 分离正负模式
    const positivePatterns = ignorePatterns.filter((p) => !p.startsWith("!"));
    const negativePatterns = ignorePatterns
        .filter((p) => p.startsWith("!"))
        .map((p) => p.slice(1));

    // 过滤出不匹配忽略模式的文件
    const filteredRelative = [];

    for (const file of relativeFiles) {
        let shouldIgnore = false;

        // 先检查正模式（忽略这些文件）
        for (const pattern of positivePatterns) {
            if (minimatch(file, pattern)) {
                shouldIgnore = true;
                break;
            }
        }

        // 然后检查负模式（即使匹配正模式，也不要忽略这些文件）
        if (shouldIgnore) {
            for (const pattern of negativePatterns) {
                if (minimatch(file, pattern)) {
                    shouldIgnore = false;
                    break;
                }
            }
        }

        if (!shouldIgnore) {
            filteredRelative.push(file);
        }
    }

    // 转换回绝对路径
    return filteredRelative.map((file) => path.resolve(rootDir, file));
}

const program = new Command();

program
    .name("sdat-flatten")
    .description("SDAT 代码库扁平化工具")
    .version("1.0.0")
    .option("-i, --input <path>", "输入目录", process.cwd())
    .option("-o, --output <path>", "输出文件路径", "flattened-codebase.xml")
    .action(async (options) => {
        const inputDir = path.resolve(options.input);
        const outputPath = path.resolve(options.output);

        console.log(`开始从 ${inputDir} 扁平化代码库`);
        console.log(`输出文件: ${outputPath}`);

        try {
            // 检查输入目录是否存在
            if (!(await fs.pathExists(inputDir))) {
                console.error(`❌ 输入目录不存在: ${inputDir}`);
                process.exit(1);
            }

            // Import ora dynamically
            const { default: ora } = await import("ora");

            // Start file discovery with spinner
            const discoverySpinner = ora("🔍 正在发现文件...").start();
            const files = await discoverFiles(inputDir);
            const filteredFiles = await filterFiles(files, inputDir);
            discoverySpinner.succeed(
                `📁 已发现 ${filteredFiles.length} 个文件`
            );

            // Process files with progress tracking
            const processingSpinner = ora("📄 正在处理文件...").start();
            const aggregatedContent = await aggregateFileContents(
                filteredFiles,
                inputDir,
                processingSpinner
            );
            processingSpinner.succeed(
                `✅ 已处理 ${aggregatedContent.processedFiles}/${filteredFiles.length} 个文件`
            );

            // Log processing results for test validation
            console.log(
                `已处理 ${aggregatedContent.processedFiles}/${filteredFiles.length} 个文件`
            );
            if (aggregatedContent.errors.length > 0) {
                console.log(`❌ 处理错误 ${aggregatedContent.errors.length}`);
            }
            console.log(`文本文件: ${aggregatedContent.textFiles.length}`);
            if (aggregatedContent.binaryFiles.length > 0) {
                console.log(
                    `二进制文件: ${aggregatedContent.binaryFiles.length}`
                );
            }

            // Generate XML output using streaming
            const xmlSpinner = ora("🔧 正在生成 XML 文件...").start();
            await generateXMLOutput(aggregatedContent, outputPath);
            xmlSpinner.succeed("📝 XML 文件生成完成");

            // Calculate and display statistics
            const outputStats = await fs.stat(outputPath);
            const stats = calculateStatistics(
                aggregatedContent,
                outputStats.size
            );

            // Display completion summary
            console.log("\n📊 完成总结:");
            console.log(
                `✅ 成功处理 ${filteredFiles.length} 个文件到 ${path.basename(outputPath)}`
            );
            console.log(`📁 输出文件: ${outputPath}`);
            console.log(`📏 总源代码大小: ${stats.totalSize}`);
            console.log(`📄 生成的 XML 文件大小: ${stats.xmlSize}`);
            console.log(`📝 总代码行数: ${stats.totalLines.toLocaleString()}`);
            console.log(`🔢 估计令牌数: ${stats.estimatedTokens}`);
            console.log(
                `📊 文件分类: ${stats.textFiles} 文本文件, ${stats.binaryFiles} 二进制文件, ${stats.errorFiles} 错误文件`
            );
        } catch (error) {
            console.error("❌ 严重错误:", error.message);
            console.error("发生了一个意外错误。");
            process.exit(1);
        }
    });

if (require.main === module) {
    program.parse();
}

module.exports = program;
